/**
 * @name Ignored return value
 * @description Ignoring return values may result in discarding errors or loss of information.
 * @kind problem
 * @tags reliability
 *       readability
 *       convention
 *       statistical
 *       non-attributable
 *       external/cwe/cwe-252
 * @problem.severity recommendation
 * @sub-severity high
 * @precision medium
 * @id py/ignored-return-value
 */

import python
import semmle.python.objects.Callables

predicate meaningful_return_value(Expr val) {
    val instanceof Name
    or
    val instanceof BooleanLiteral
    or
    exists(FunctionValue callee |
        val = callee.getACall().getNode() and returns_meaningful_value(callee)
    )
    or
    not exists(FunctionValue callee | val = callee.getACall().getNode()) and not val instanceof Name
}

/* Value is used before returning, and thus its value is not lost if ignored */
predicate used_value(Expr val) {
    exists(LocalVariable var, Expr other |
        var.getAnAccess() = val and other = var.getAnAccess() and not other = val
    )
}

predicate returns_meaningful_value(FunctionValue f) {
    not exists(f.getScope().getFallthroughNode()) and
    (
        exists(Return ret, Expr val | ret.getScope() = f.getScope() and val = ret.getValue() |
            meaningful_return_value(val) and
            not used_value(val)
        )
        or
        /*
         * Is f a builtin function that returns something other than None?
         * Ignore __import__ as it is often called purely for side effects
         */

        f.isBuiltin() and
        f.getAnInferredReturnType() != ClassValue::nonetype() and
        not f.getName() = "__import__"
    )
}

/* If a call is wrapped tightly in a try-except then we assume it is being executed for the exception. */
predicate wrapped_in_try_except(ExprStmt call) {
    exists(Try t |
        exists(t.getAHandler()) and
        strictcount(Call c | t.getBody().contains(c)) = 1 and
        call = t.getAStmt()
    )
}

from ExprStmt call, FunctionValue callee, float percentage_used, int total
where
    call.getValue() = callee.getACall().getNode() and
    returns_meaningful_value(callee) and
    not wrapped_in_try_except(call) and
    exists(int unused |
        unused = count(ExprStmt e | e.getValue().getAFlowNode() = callee.getACall()) and
        total = count(callee.getACall())
    |
        percentage_used = (100.0 * (total - unused) / total).floor()
    ) and
    /* Report an alert if we see at least 5 calls and the return value is used in at least 3/4 of those calls. */
    percentage_used >= 75 and
    total >= 5
select call,
    "Call discards return value of function $@. The result is used in " + percentage_used.toString() +
        "% of calls.", callee, callee.getName()
